package com.zian.aspect;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson2.JSON;
import com.zian.annotation.OperLog;
import com.zian.common.CommonConstant;
import com.zian.context.BaseContext;
import com.zian.domain.po.SysAdminUser;
import com.zian.domain.po.SysOperLog;
import com.zian.domain.po.SysUser;
import com.zian.enums.BusinessStatus;
import com.zian.manager.AsyncManager;
import com.zian.manager.factory.AsyncFactory;
import com.zian.service.ISysOperLogService;
import com.zian.utils.RedisUtils;
import com.zian.utils.ServletUtils;
import com.zian.utils.ip.AddressUtils;
import com.zian.utils.ip.IpUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.TimerTask;

@Slf4j
@Aspect // 切面类
@Component
@RequiredArgsConstructor
public class LogAspect {


    private final RedisUtils redisUtils;

    /**
     * 计算操作消耗时间
     */
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");


    /**
     * 前置增强：记录方法执行开始时间
     *
     * @param joinPoint
     */
    @Before("@annotation(operLog)")
    public void boBefore(JoinPoint joinPoint, OperLog operLog) {
        // 记录请求时间，也就是方法的开始执行时间
        TIME_THREADLOCAL.set(System.currentTimeMillis());


    }

    /**
     * 后置增强：方法执行成功后记录日志
     *
     * @param joinPoint  执行方法
     * @param operLog    方法上注解信息
     * @param jsonResult 执行方法的返回值
     */
    @AfterReturning(pointcut = "@annotation(operLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, OperLog operLog, Object jsonResult) {
        handleLog(joinPoint, operLog, null, jsonResult);
    }


    /**
     * 后置增强：方法执行失败后记录日志
     *
     * @param joinPoint 执行方法
     * @param operLog   方法上注解信息
     * @param e         执行方法的异常信息
     */
    @AfterThrowing(pointcut = "@annotation(operLog)", throwing = "e")
    public void doAfterReturning(JoinPoint joinPoint, OperLog operLog, Exception e) {
        log.info("进入 [ LogAspect ] 前置增强 ， 记录方法开始执行时间");
        handleLog(joinPoint, operLog, e, null);
    }


    /**
     * 无论方法执行成功了还是失败，都会执行此方法
     *
     * @param joinPoint  切点，方法的相关数据都在里面
     * @param operLog    注解信息
     * @param e          如果是执行异常，异常错误信息
     * @param jsonResult 如果执行成功，方法的返回值
     */
    protected void handleLog(final JoinPoint joinPoint, OperLog operLog, final Exception e, Object jsonResult) {
        try {
            // 获取当前的用户
            Long currentId = BaseContext.getCurrentId();
            String uuid = (String) redisUtils.getCacheObject(CommonConstant.LOGIN_ADMIN_USERID_KEY + currentId);
            Serializable userInfo = redisUtils.getCacheObject(CommonConstant.LOGIN_ADMIN_USER_KEY + uuid);

            // *========数据库日志=========*//
            SysOperLog dbOperLog = new SysOperLog();
            dbOperLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // ordinal 枚举的索引，从0开始
            // 请求的地址
            String ip = IpUtils.getIpAddr();
            dbOperLog.setOperIp(ip);
            // 截取的示例：/system/menu/list
            dbOperLog.setOperUrl(StrUtil.sub(ServletUtils.getRequest().getRequestURI(), 0, 255));
            // 记录操作人的信息
            if (userInfo != null) {
                SysAdminUser loginUser = (SysAdminUser) userInfo;
                dbOperLog.setOperName(loginUser.getUsername());

            }

            if (e != null) {
                // 修改操作日志的状态信息
                dbOperLog.setStatus(BusinessStatus.FAIL.ordinal());
                // 异常信息
                dbOperLog.setErrorMsg(StrUtil.sub(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            // 获取方法的全限定符 = 类名+方法名
            dbOperLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            dbOperLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, operLog, dbOperLog, jsonResult);
            // 设置消耗时间
            dbOperLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
            // 保存数据库 , 调用线程是来执行新增日志操作
            AsyncManager.me().execute(new TimerTask() {
                @Override
                public void run() {
                    // 远程查询操作地点
                    dbOperLog.setOperLocation(AddressUtils.getRealAddressByIP(dbOperLog.getOperIp()));
                    SpringUtil.getBean(ISysOperLogService.class).insertOperlog(dbOperLog);
                }
            });
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        } finally {
            TIME_THREADLOCAL.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param operLog   注解信息
     * @param dbOperLog 操作日志对象
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, OperLog operLog, SysOperLog dbOperLog, Object jsonResult) throws Exception {
        // 设置action动作
        dbOperLog.setBusinessType(operLog.businessType().ordinal());
        // 设置标题
        dbOperLog.setTitle(operLog.title());

        // 获取参数的信息，传入到数据库中。
        setRequestValue(joinPoint, dbOperLog);

        // 是否需要保存response，参数和值
        if (jsonResult != null) {
            dbOperLog.setJsonResult(StrUtil.sub(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数，放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
        String params = argsArrayToString(joinPoint.getArgs());
        operLog.setOperParam(StrUtil.sub(params, 0, 2000));
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null) {
            for (Object o : paramsArray) {
                if (o != null) {
                    try {
                        String jsonObj = JSON.toJSONString(o);
                        params.append(jsonObj).append(" ");
                    } catch (Exception e) {
                        log.warn("参数拼接异常");
                    }
                }
            }
        }
        return params.toString().trim();
    }


}
